/**
 * BeanMap
 *
 * @author Chris Pratt
 *
 * 11/11/2011
 */
package com.anodyzed.onyx.bean;

import com.anodyzed.onyx.log.Log;
import com.anodyzed.onyx.log.LogBuilder;
import com.anodyzed.onyx.type.Converter;
import com.anodyzed.onyx.type.ConverterFactory;

import java.beans.IntrospectionException;
import java.io.Serializable;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;


@SuppressWarnings("serial")
public class BeanMap implements BeanWrapper, Map<String,Object>, Serializable {
  private static final Log log = LogBuilder.getLogger();
  
  private Object bean;
  private Set<String> keys = null;
  private ConverterFactory factory;

  /**
   * Constructor
   *
   * @param bean The Source Data Bean 
   * @param factory The Type Conversion Factory 
   */
  public BeanMap (Object bean,ConverterFactory factory) {
    this.bean = bean;
    this.factory = factory;
  } //BeanMap

  /**
   * Constructor
   *
   * @param bean The Source Data Bean
   */
  public BeanMap (Object bean) {
    this(bean,new ConverterFactory());
  } //BeanMap

  /**
   * Get the value of the bean attribute. 
   *
   * @return The bean value as a Object
   */
  @Override
  public Object getBean () {
    return bean;
  } //getBean
  
  /**
   * Set the value of the bean attribute.
   *
   * @param bean The new bean value as a Object
   */
  public void setBean (Object bean) {
    this.bean = bean;
  } //setBean

  /**
   * Add an additional converter to the Converter Factory
   *
   * @param a The first conversion type 
   * @param b The second conversion type 
   * @param converter The Converter 
   */
  public void addConverter (Class<?> a,Class<?> b,Converter converter) {
    factory.addConverter(a,b,converter);
  } //addConverter

  /**
   * Get the Converter Factory used in this Bean Map
   *
   * @return Converter Factory 
   */
  public ConverterFactory getConverterFactory () {
    return factory;
  } //getConverterFactory

  /**
   * Set the Converter Factory to use in this Bean Map
   *
   * @param factory The new Converter Factory
   */
  public void setConverterFactory (ConverterFactory factory) {
    this.factory = factory;
  } //setConverterFactory

  /** 
   * Returns the number of properties defined by the bean. 
   *  
   * Note: This does not take into account sub-properties of contained beans 
   * that could be reached using dot notation 
   * 
   * @return property count
   * @see java.util.AbstractMap#size()
   */
  @Override
  public int size () {
    return entrySet().size();
  } //size

  /** 
   * Returns true if there are no properties defined by the bean. 
   * 
   * @return false if any properties are defined on the bean
   * @see java.util.AbstractMap#isEmpty()
   */
  @Override
  public boolean isEmpty () {
    return size() == 0;
  } //isEmpty

  /** 
   * Determine whether any of the properties of this bean match the supplied 
   * value. 
   *  
   * Note: the search will include any Collections or Maps in the bean, but not 
   * sub-beans. 
   * 
   * @param value The value to search for
   * @return true if the value is contained in this bean
   * @see java.util.AbstractMap#containsValue(java.lang.Object)
   */
  @Override
  public boolean containsValue (Object value) {
    Object v;
    for(String key : keySet()) {
      if((v = get(key)) == null) {
        if(value == null) {
          return true;
        }
      } else {
        if(v.equals(value)){
          return true;
        }
        if(v instanceof Map) {
          if(((Map<?,?>)v).containsValue(value)) {
            return true;
          }
        } else if(v instanceof Collection) {
          if(((Collection<?>)v).contains(value)) {
            return true;
          }
        }
      }
    }
    return false;
  } //containsValue

  /**
   * Determine whether the supplied key maps to a property of this bean, or a 
   * sub-bean using dot-notation. 
   *  
   * @param key The bean property key
   * @return true if the key is contained in the bean hierarchy
   * @see java.util.AbstractMap#containsKey(java.lang.Object)
   */
  @Override
  public boolean containsKey (Object key) {
    try {
      return (key != null) && BeanUtils.contains(bean,key.toString(),factory);
    } catch(IntrospectionException x) {
      log.debug("Unable to locate the value for ''{0}'' in bean {1}",key,bean,x);
    }
    return false;
  } //containsKey

  /** 
   * Returns the value for the supplied property of this bean, or a sub-bean 
   * using dot-notation. 
   * 
   * @param key The bean property key
   * @return The bean property value
   * @see java.util.AbstractMap#get(java.lang.Object)
   */
  @Override
  public Object get (Object key) {
    try {
      return (key != null) ? BeanUtils.getValue(bean,key.toString(),factory) : null;
    } catch(IntrospectionException x) {
      log.debug("Unable to retrieve the value for ''{0}'' in bean {1}",key,bean,x);
    }
    return null;
  } //get

  /** 
   * Set the value of the supplied property of this bean, or a sub-bean using 
   * dot-notation.  Any null beans found along the dot-notation path will be 
   * instantiated as needed. 
   * 
   * @param key The bean property key
   * @param value The updated bean property value
   * @return The previous bean property value
   * @see java.util.AbstractMap#put(java.lang.Object, java.lang.Object)
   */
  @Override
  public Object put (String key,Object value) {
    try {
      return (key != null) ? BeanUtils.setValue(bean,key,value,factory) : null;
    } catch(IntrospectionException x) {
      log.debug("Unable to update the value for ''{0}'' in bean {1} to {2}",key,bean,value,x);
    }
    return null;
  } //put

  /** 
   * If the key references a property with an Object type, set that property to
   * null. Otherwise, attempt to set the property to a neutral value (i.e. 
   * int=0, boolean=false...) 
   * 
   * Note: this does not meet the contract of the Map interface, since it does 
   * not remove the corresponding key.
   * 
   * @param key The bean property key
   * @return the removed property value
   * @see java.util.AbstractMap#remove(java.lang.Object)
   */
  @Override
  public Object remove (Object key) {
    try {
      return (key != null) ? BeanUtils.setValue(bean,key.toString(),null,factory) : null;
    } catch(IntrospectionException x) {
      log.debug("Unable to remove the value for ''{0}'' in bean {1}",key,bean,x);
    }
    return null;
  } //remove

  /** 
   * Put each of the values in the supplied map into this map. 
   * 
   * @param map The source map
   * @see java.util.AbstractMap#putAll(java.util.Map)
   */
  @Override
  public void putAll (Map<? extends String,?> map) {
    for(Entry<? extends String,?> entry : map.entrySet()) {
      put(entry.getKey(),entry.getValue());
    }
  } //putAll

  /** 
   * Removes all the values from the map. 
   *  
   * Note: this does not meet the contract of the Map interface, since it does 
   * not remove the corresponding keys.
   * 
   * @see java.util.AbstractMap#clear()
   */
  @Override
  public void clear () {
    for(String key : keySet()) {
      put(key,null);
    }
  } //clear

  /** 
   * Get the Set of property names in this bean. 
   *  
   * Note: this set does not include properties of sub-beans that could be 
   * referenced using dot-notation 
   * 
   * @return Set of property name Strings
   * @see java.util.AbstractMap#keySet()
   */
  @Override
  @SuppressWarnings("unchecked")
  public Set<String> keySet () {
    if(bean instanceof Map<?,?>) {
      return ((Map<String,?>)bean).keySet();
    } else {
      if(keys == null) {
        try {
          keys = BeanUtils.getProperties(bean.getClass());
        } catch(IntrospectionException x) {
          log.debug("Failed building the keySet for {0}",bean,x);
        }
      }
      return keys;
    }
  } //keySet

  /** 
   * Get the Collection of property values in this bean. 
   * 
   * @return Collection of property values
   * @see java.util.AbstractMap#values()
   */
  @Override
  @SuppressWarnings("unchecked")
  public Collection<Object> values () {
    if(bean instanceof List<?>) {
      return (List<Object>)bean;
    } else if(bean instanceof Map<?,?>) {
      return ((Map<?,Object>)bean).values();
    } else {
      List<Object> vals = new ArrayList<Object>();
      for(String key : keySet()) {
        vals.add(get(key));
      }
      return vals;
    }
  } //values

  /** 
   * Get the Set of key/value entry pairs in this map 
   * 
   * @return Set of Map Entry's
   * @see java.util.AbstractMap#entrySet()
   */
  @Override
  @SuppressWarnings("unchecked")
  public Set<Entry<String,Object>> entrySet () {
    if(bean instanceof Map<?,?>) {
      return ((Map<String,Object>)bean).entrySet();
    } else {
      Set<Entry<String,Object>> entries = new HashSet<Entry<String,Object>>();
      for(String key : keySet()) {
        entries.add(new AbstractMap.SimpleEntry<String,Object>(key,get(key)));
      }
      return entries;
    }
  } //entrySet

  /** 
   * Determine whether the supplied argument is equivalent to this BeanMap 
   * 
   * @param o The Object in question
   * @return true if <code>o</code> is a BeanMap containing an equivalent Bean
   * @see java.util.AbstractMap#equals(java.lang.Object)
   */
  @Override
  public boolean equals (Object o) {
    return (o instanceof BeanMap) && (bean.equals(((BeanMap)o).getBean()));
  } //equals

  /** 
   * Generate a Hash Bucket Index for this BeanMap 
   * 
   * @return bean based hash code
   * @see java.util.AbstractMap#hashCode()
   */
  @Override
  public int hashCode () {
    return bean.hashCode() * 31;
  } //hashCode

  /** 
   * Return a String Representation of this bean map
   * 
   * @return Debug String
   * @see java.util.AbstractMap#toString()
   */
  @Override
  public String toString () {
    return "BeanMap<" + bean + '>';
  } //toString

} //*BeanMap
